// Copyright 2020 Apulis Technology Inc. All rights reserved.

package s3

import (
	"context"
	"fmt"
	"github.com/apulis/sdk/go-utils/storage/comm"
	_ "github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"io"
	"io/ioutil"
	"os"
	"strings"
)

type MethodBackendS3 struct {
	Client *s3.Client
	Cfg    comm.HandlerCfg
}

func (s *MethodBackendS3) Init(cfg comm.HandlerCfg) error {
	s.Cfg = cfg

	// Load the Shared AWS Configuration (~/.aws/config)
	// Load the Shared AWS Credentials (~/.aws/credentials)
	s3config, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return err
	}

	s.Client = s3.NewFromConfig(s3config)
	return nil
}

func (s *MethodBackendS3) StoreFile(localFilePath string, dst string, perm os.FileMode) error {
	if !s.isValidSchema(dst) {
		return comm.ErrTypeInvalidSchema
	}

	bucket, err := s.getBucket(dst)
	if err != nil {
		return err
	}

	key, err := s.getObject(dst)
	if err != nil {
		return err
	}

	f, err := os.Open(localFilePath)
	if err != nil {
		return err
	}
	defer f.Close()

	err = s.autoCreateBucket(bucket)
	if err != nil {
		return err
	}

	input := &s3.PutObjectInput{
		Bucket: &bucket,
		Key:    &key,
		Body:   f,
	}

	result, err := s.Client.PutObject(context.TODO(), input)
	if err != nil {
		return err
	}

	if s.Cfg.Debug {
		fmt.Printf("PubObject result = %+v", result)
	}
	return err
}

func (s *MethodBackendS3) GetFile(src string, dstLocalPath string, perm os.FileMode) error {
	if !s.isValidSchema(src) {
		return comm.ErrTypeInvalidSchema
	}

	if s.isFileExist(dstLocalPath) {
		return comm.ErrTypeDstFileExist
	}

	bucket, err := s.getBucket(src)
	if err != nil {
		return err
	}

	key, err := s.getObject(src)
	if err != nil {
		return err
	}

	input := &s3.GetObjectInput{
		Bucket: &bucket,
		Key:    &key,
	}

	result, err := s.Client.GetObject(context.TODO(), input)
	if err != nil {
		return err
	}

	if s.Cfg.Debug {
		fmt.Printf("getObject result = %+v", result)
	}

	content, err := ioutil.ReadAll(result.Body)
	if err != nil {
		return err
	}
	defer result.Body.Close()

	err = ioutil.WriteFile(dstLocalPath, content, perm)
	if err != nil {
		return fmt.Errorf("%v", err)
	}

	return err
}

func (s *MethodBackendS3) OpenFile(src string) (io.ReadCloser, error) {
	if !s.isValidSchema(src) {
		return nil, comm.ErrTypeInvalidSchema
	}

	bucket, err := s.getBucket(src)
	if err != nil {
		return nil, err
	}

	key, err := s.getObject(src)
	if err != nil {
		return nil, err
	}

	input := &s3.GetObjectInput{
		Bucket: &bucket,
		Key:    &key,
	}

	result, err := s.Client.GetObject(context.TODO(), input)
	if err != nil {
		return nil, err
	}

	if s.Cfg.Debug {
		fmt.Printf("getObject result = %+v", result)
	}

	return result.Body, nil
}

func (s *MethodBackendS3) CopyFile(src string, dst string) error {
	if !s.isValidSchema(src) || !s.isValidSchema(dst) {
		return comm.ErrTypeInvalidSchema
	}

	srcObjWithBucket, err := comm.GetSchemaFile(src)
	if err != nil {
		return err
	}

	dstBucket, err := s.getBucket(dst)
	if err != nil {
		return err
	}

	dstObject, err := s.getObject(dst)
	if err != nil {
		return err
	}

	input := &s3.CopyObjectInput{
		Bucket:     &dstBucket,
		Key:        &dstObject,
		CopySource: &srcObjWithBucket,
	}

	result, err := s.Client.CopyObject(context.TODO(), input)
	if err != nil {
		return err
	}

	if s.Cfg.Debug {
		fmt.Printf("copyObject result = %+v", result)
	}
	return nil
}

func (s *MethodBackendS3) MoveFile(src string, dst string) error {
	err := s.CopyFile(src, dst)
	if err != nil {
		return err
	}
	_ = s.DeleteFile(src)
	return nil
}

func (s *MethodBackendS3) DeleteFile(dst string) error {
	if !s.isValidSchema(dst) {
		return comm.ErrTypeInvalidSchema
	}

	bucket, err := s.getBucket(dst)
	if err != nil {
		return err
	}

	object, err := s.getObject(dst)
	if err != nil {
		return err
	}

	input := &s3.DeleteObjectInput{
		Bucket: &bucket,
		Key:    &object,
	}

	result, err := s.Client.DeleteObject(context.TODO(), input)
	if err != nil {
		return err
	}

	if s.Cfg.Debug {
		fmt.Printf("DeleteObject result = %+v", result)
	}
	return nil
}

func (s *MethodBackendS3) CopyDir(srcDir string, dstDir string) error {
	return fmt.Errorf("%v", comm.ErrTypeNotImplemented)
}

func (s *MethodBackendS3) CopyFromLocalDir(srcDirLocal string, dstDir string) error {
	return fmt.Errorf("%v", comm.ErrTypeNotImplemented)
}

func (s *MethodBackendS3) CopyFromRemoteDir(srcDir string, dstDirLocal string) error {
	return fmt.Errorf("%v", comm.ErrTypeNotImplemented)
}

func (s *MethodBackendS3) MoveDir(srcDir string, dstDir string) error {
	return fmt.Errorf("%v", comm.ErrTypeNotImplemented)
}

func (s *MethodBackendS3) MoveFromLocalDir(srcDirLocal string, dstDir string) error {
	return fmt.Errorf("%v", comm.ErrTypeNotImplemented)
}

func (s *MethodBackendS3) MoveFromRemoteDir(srcDir string, dstDirLocal string) error {
	return fmt.Errorf("%v", comm.ErrTypeNotImplemented)
}

// 对S3来说，无需创建目录
func (s *MethodBackendS3) Mkdir(dstDir string) error {
	return nil
}

func (s *MethodBackendS3) DeleteDir(dstDir string, force bool) error {
	return fmt.Errorf("%v", comm.ErrTypeNotImplemented)
}

// internal function
func (s *MethodBackendS3) isValidSchema(resource string) bool {
	schema, err := comm.GetSchema(resource)
	if err != nil {
		return false
	}

	return schema == comm.SCHEMA_TYPE_S3
}

func (s *MethodBackendS3) isFileExist(localFile string) bool {
	_, err := os.Stat(localFile)
	if err == nil {
		return true
	}

	return false
}

func (s *MethodBackendS3) getBucket(resource string) (string, error) {
	f, err := comm.GetSchemaFile(resource)
	if err != nil {
		return "", nil
	}

	parts := strings.Split(f, "/")
	if len(parts) < 2 {
		return "", comm.ErrGetS3Bucket
	}

	return parts[0], nil
}

func (s *MethodBackendS3) getObject(resource string) (string, error) {
	f, err := comm.GetSchemaFile(resource)
	if err != nil {
		return "", nil
	}

	parts := strings.SplitN(f, "/", 2)
	if len(parts) < 2 {
		return "", comm.ErrGetS3Bucket
	}
	return parts[1], nil
}

func (s *MethodBackendS3) autoCreateBucket(bucket string) error {
	input := &s3.CreateBucketInput{
		Bucket: &bucket,
	}

	result, err := s.Client.CreateBucket(context.TODO(), input)
	if err != nil {
		return nil
	}

	if s.Cfg.Debug {
		fmt.Printf("CreateBucket result = %+v", result)
	}
	return nil
}
